package com.gitee.feizns.wx.service.wechat.impl;

import com.gitee.feizns.web.utils.EncryptUtils;
import com.gitee.feizns.wx.service.ex.RequestAccessTokenFailException;
import com.gitee.feizns.wx.domain.props.WeChatProperties;
import com.gitee.feizns.wx.domain.vo.JsApiTicketVo;
import com.gitee.feizns.wx.service.ex.RequestJsApiTicketFailException;
import com.gitee.feizns.wx.service.wechat.AuthorizeToken;
import com.gitee.feizns.wx.service.wechat.AuthorizeUser;
import com.gitee.feizns.wx.service.wechat.FaJsApiTicket;
import com.gitee.feizns.wx.service.wechat.Message;
import com.gitee.feizns.wx.service.wechat.RemoteWeChatService;
import com.gitee.feizns.wx.service.wechat.Token;
import com.gitee.feizns.wx.service.wechat.WeChatUserInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author feizns
 * @since 2019/4/12
 */
public class RemoteChatServiceImpl implements RemoteWeChatService {

    private static final Logger logger = LoggerFactory.getLogger(RemoteChatServiceImpl.class);

    private static final long CACHE_TIME = TimeUnit.MILLISECONDS.convert(100, TimeUnit.MINUTES);

    /**
     * 缓存无效判断
     */
    private static final long TEN_MINUTES = (TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES));

    /**
     * access_token
     */
    private Token token;

    /**
     * token 的有效截至时间
     */
    private long tokenEndTime;

    /**
     * 微信js api ticket
     */
    private FaJsApiTicket jsApiTicket;

    /**
     * ticket 的有效截至时间
     */
    private long ticketEndTime;

    /**
     * appId
     */
    private String appId;

    /**
     * 微信密钥
     */
    private String appSecret;

    /**
     * 微信相关配置
     */
    private WeChatProperties properties;

    /**
     *
     */
    private RestTemplate restTemplate;

    public RemoteChatServiceImpl(String appId, String appSecret, WeChatProperties properties, RestTemplate restTemplate) {
        this.appId = appId;
        this.appSecret = appSecret;
        this.properties = properties;
        this.restTemplate = restTemplate;
    }

    /**
     * 获取 access_token
     * @return
     */
    @Override
    public String getAccessToken() {
        return getToken().getAccess_token();
    }

    /**
     * 获取js api 签名权限
     * @param url
     * @return
     */
    @Override
    public JsApiTicketVo getJsApiTicket(String url) {
        return createFvJsApiTicket(url);
    }

    /**
     * 获取公众号 appId
     * @return
     */
    @Override
    public String getAppId() {
        return appId;
    }

    @Override
    public String getAppSecret() {
        return appSecret;
    }

    @Override
    public AuthorizeToken getAuthorizeToken(String code) {
        return restTemplate.getForObject(properties.getAuthorizeUrl(), AuthorizeToken.class,
                appId,
                appSecret,
                code);
    }

    @Override
    public AuthorizeUser getAuthorizeUserInfo(String code) {
        AuthorizeToken token = getAuthorizeToken(code);
        return restTemplate.getForObject(properties.getAuthorizeUserInfoUrl(), AuthorizeUser.class,
                token.getAccess_token(),
                token.getOpenId());
    }

    @Override
    public WeChatUserInfo getUserInfo(String openId) {
        Token token = getToken();
        return restTemplate.getForObject(properties.getUserInfoUrl(), WeChatUserInfo.class,
                token.getAccess_token(), openId);
    }

    @Override
    public WeChatUserInfo getUserInfoByCode(String code) {
        AuthorizeToken token = getAuthorizeToken(code);
        return getUserInfo(token.getOpenId());
    }

    private JsApiTicketVo createFvJsApiTicket(String url) {
        JsApiTicketVo ticket = new JsApiTicketVo();
        ticket.setAppId( appId );
        ticket.setNonceStr( UUID.randomUUID().toString() );
        ticket.setTimestamp( System.currentTimeMillis() / 1000 );
        ticket.setSignature( generatorSignature(ticket.getTimestamp().toString(), ticket.getNonceStr(), url) );
        return ticket;
    }

    /**
     * 生成签名
     * @param timestamp
     * @param nonceStr
     * @param url
     * @return
     */
    private String generatorSignature(String timestamp, String nonceStr, String url) {
        String result = MessageFormat.format(
                properties.getJsApiSignatureUrl(),
                getJsApiTicket().getTicket(),
                nonceStr,
                timestamp,
                url);
        return EncryptUtils.sha1(result);
    }

    /**
     * 获取token 没有或过期就请求微信
     * @return
     */
    public Token getToken() {
        if ( token == null || isInvalid(tokenEndTime) ) {
            token = restTemplate.getForObject(properties.getAccessTokenUrl(), Token.class, appId, appSecret);
            if ( token.isFail() )
                throw new RequestAccessTokenFailException(getErrMsg(token));

            tokenEndTime = System.currentTimeMillis() + CACHE_TIME;
            logger.info("WeChat Request   => " + token.toString());
        } else {
            logger.info("WeChat Cache hit => " + token.toString());
        }

        return token;
    }

    public FaJsApiTicket getJsApiTicket() {
        if ( jsApiTicket == null || isInvalid(ticketEndTime) ) {
            jsApiTicket = restTemplate.getForObject(properties.getJsApiTicketUrl(), FaJsApiTicket.class, getAccessToken());
            if ( jsApiTicket.isFail() )
                throw new RequestJsApiTicketFailException(getErrMsg(jsApiTicket));

            ticketEndTime = System.currentTimeMillis() + CACHE_TIME;
            logger.info("WeChat Request   => " + jsApiTicket.toString());
        } else {
            logger.info("WeChat Cache hit => " + jsApiTicket.toString());
        }

        return jsApiTicket;
    }

    private String getErrMsg(Message message) {
        return String.format("CODE: %s MSG: %s", message.getErrcode(), message.getErrmsg());
    }

    /**
     * 验证是否有效，提前10分钟
     * @param endTime
     * @return
     */
    private static final boolean isInvalid(long endTime) {
        return System.currentTimeMillis() >= endTime;
    }

}
